SQL injection יכולה להרוס לכם את האתר, הגנה מפניה היא בבסיס אבטחת האתר. המדריך הבא יסביר כיצד היא פועלת וכיצד ניתן להתגונן מפניה.
מה זה SQL Injection?
SQL היא שפה לתקשורת עם בסיסי נתונים מבוססי טבלאות, היא מאפשרת ליצור, למחוק, לשנות ערכים ושדות של טבלאות ולנהל מידע בצורה יעילה ומהירה. רוב אתרי ה-PHP משתמשים בבסיס נתונים בשם MySQL בכדי לנהל את המידע של האתר. אך פעמים רבות נוצרות בעיות אבטחה בשימוש לא נכון ב-SQL, לדוגמה ניקח את קוד התחברות ללוח הניהול הבא:
$conn = mysql_connect('localhost', 'user', 'password');
mysql_select_db("database", $conn);
if (isset($_POST['submit'])) {
$username = $_POST['username'];
$password = $_POST['password'];
$result = mysql_query("SELECT * FROM Users where username='$username' and password='$password'");
if (mysql_fetch_row($result))
echo “Access Granted”;
else
echo “Access Denied”;
}
mysql_select_db("database", $conn);
if (isset($_POST['submit'])) {
$username = $_POST['username'];
$password = $_POST['password'];
$result = mysql_query("SELECT * FROM Users where username='$username' and password='$password'");
if (mysql_fetch_row($result))
echo “Access Granted”;
else
echo “Access Denied”;
}
PHP מחליפה את המשתנים $username ו-$password בערכים שמתקבלים ב-POST כך שאם נשים לדוגמה את הערך admin בשדה שם המשתמש ו-123456 בשדה הסיסמה השאילתא שתיווצר היא זו:
SELECT * FROM Users WHERE username='admin' AND password='123456'
אך מה יקרה אם במקום להכניס שם משתמש וסיסמה סטנדרטיים שהמערכת מצפה להם נספק את הערך admin' עם גרש יחיד בסוף? השאילתא שתיצוור היא:
SELECT * FROM Users WHERE username='admin'' AND PASSWORD='123456'
שתגרום לשגיאת SQL כיוון שפתחנו מחרוזת (הגרש היחיד) בלי לסגור אותה.
מה שקורה הוא שהתוכן שנמצא בשדה שם המשתמש מפורש על ידי mysql_query כחלק מהתחביר של פקודת ה-SQL ומריץ אותה ככזו. תוקף יכול להשתמש בזה בשביל ליצור שאילתות מסוכנות יותר כמו לדוגמה במקרה שבו התוקף מכניס בשדה שם המשתמש את התוכן הבא:
' or 1=1--
השאילתא שתיווצר במקרה הזה היא:
SELECT * FROM Users WHERE username='' or 1=1--' AND password=''
הגרש היחיד משמש לסגירת המחרוזת כך שנוצרת שאילתא חדשה שאומרת לבסיס הנתונים, “אני מעוניין בשורות שבהם התנאי שם המשתמש שווה לכלום או במקרה שבו התנאי 1=1 נכון”, מכיוון ש-1 תמיד יהיה שווה ל-1 השאילתא תחזיר את כל המשתמשים במערכת. המקף הכפול בסוף השאילתא אומר ל-MySQL להתעלם משאר השאילתא בשביל למנוע שגיאת SQL.
ניתן ליצור שאילתות מורכבות יותר שיספקו לתוקף את כל השדות והשורות בטבלאות שלכם, לקרוא קבצים שנמצאים על השרת או אפילו במקרים מסויימים להעלות קבצים שיאפשרו שליטה מלאה על השרת.
חשוב להבין שכל קלט שמגיע מהמשתמש הוא פוטנציאלית מסוכן ולא רק $_POST ו_$_GET. לדוגמה במקרה שבו כתבנו אפליקציית דיבוג שנותנת לנו מידע על דפים שלא נמצאו (404) או שגיאות (500) נרצה לשמור את ה-URL שיצר את השגיאה בבסיס הנתונים. אם השאילתות לא מוגנות המערכת תהיה פגיעה ל-URL מסוכן שמכיל קוד SQL. זה נכון גם לגבי עוגיות שיכולות להכיל מידע בעייתי או ה-referrer או ה-User Agent של המשתמש. כל מידע שמגיע מהמשתמש הוא פוטנציאלית מסוכן ודורש הגנה ראויה.
הגנה מפני SQL Injection
mysql_real_escape_string
ישנן שתי דרכים להגנה מפני המתקפה. הנפוצה אך הפחות בטוחה היא שימוש בפונקציה mysql_real_escape_string שמוסיפה סלאש הפוך (\) ליד תווים בעייתים כך שהם לא מפורשים כתווים מיוחדים בשפת SQL. לדוגמה:
$username = mysql_real_escape_string($_POST['username']);
$password = mysql_real_escape_string($_POST['password']);
$result = mysql_query("SELECT * FROM Users WHERE username='$username' and password='$password'");
if (mysql_fetch_row($result))
echo "access granted";
else
echo "access denied";
$password = mysql_real_escape_string($_POST['password']);
$result = mysql_query("SELECT * FROM Users WHERE username='$username' and password='$password'");
if (mysql_fetch_row($result))
echo "access granted";
else
echo "access denied";
הקוד הבא ימנע SQL Injection כיוון שכאשר התוקף ינסה להשתמש בתווים מיוחדים בשביל ליצור שאילתות SQL זה ימנע ממנו, באמצעות הברחת תווים מיוחדים:
echo mysql_real_escape_string("admin' or 1=1--");
output: admin\' or 1=1--
output: admin\' or 1=1--
אך הפונקציה הזו לא בטוחה בכל המקרים כמו למשל במקרה שמדובר בשדה מספרי ולא נבדק שהערך הוא ערך מספרי:
$id = mysql_real_escape_string($_GET['id']);
$result = mysql_query("SELECT page,title,date from Articles where id=$id");
$result = mysql_query("SELECT page,title,date from Articles where id=$id");
במקרה כזה האפליקציה עדיין פגיעה ל-SQL Injection כיוון שניתן להחדיר פקודות SQL ללא תווים מיוחדים, לדוגמה:
-1 union select null,null,null from Articles--
שתיצור את השאילתא הבאה:
mysql_query(“SELECT page,title,date from Articles where id=-1 union select null,null from Articles--”);
ערכים שהם לא מחרוזות צריכים לבדוק או להתייחס אליהם כמחרוזות בתוך השאילתא באמצעות הוספת גרש, המתקפה למעלה יכולה להימנע באמצעות:
mysql_query(“SELECT page,title,date from Articles where id='$id');
כיוון שישנן מרכאות MySQL יתייחס לתוכן כמחרוזת, ותווים בעיתים יטופלו על mysql_real_escape_string.
Parameterized Statements ו-PDO
הדרך הנכונה והמומלצת לעבוד בכדי למנוע SQL Injection היא שימוש בטכניקה שנקראת parameterized statements שבה ישנה הפרדה בין הקוד לבין הערכים. ב-PHP ישנו API לבסיסי נתונים בשם PDO שיש לו הרבה יתרונות על פונקציות ה-mysql_*. האובייקט מאפשר לתקשר עם יותר מבסיס נתונים אחד למשל עם sqlite או orcale ולא רק עם MySQL. יש לו יתרונות נוספים כמו מהירות, אך לצורך המדריך היתרון הכי גדול שלו הוא השימוש ב-parametrized statements.
הנה דוגמה לשימוש ב-PDO:
$dbhost = 'localhost';
$dbname = 'database';
$dbuser = 'user';
$dbpass = 'pass';
$pdo_info = "mysql:host=$dbhost;dbname=$dbname";
$conn = new PDO($pdo_info, $dbuser, $dbpass);
$st = $conn->prepare("SELECT * FROM Users WHERE username=? and password=?");
$st->setFetchMode(PDO::FETCH_ASSOC);
$st->bindParam(1, $_POST['username']);
$st->bindParam(2, $_POST['password']);
$st->execute();
while ($r = $st->fetch())
print_r($r);
$dbname = 'database';
$dbuser = 'user';
$dbpass = 'pass';
$pdo_info = "mysql:host=$dbhost;dbname=$dbname";
$conn = new PDO($pdo_info, $dbuser, $dbpass);
$st = $conn->prepare("SELECT * FROM Users WHERE username=? and password=?");
$st->setFetchMode(PDO::FETCH_ASSOC);
$st->bindParam(1, $_POST['username']);
$st->bindParam(2, $_POST['password']);
$st->execute();
while ($r = $st->fetch())
print_r($r);
$pdo_info זה הפרמטר הראשון ביצירה של אובייקט PDO חדש, הוא מכיל את בסיס הנתונים המדובר, במקרה הזה mysql, לאחר מכן נקודתיים והפרמטרים host ו-dbname. הארגיומנט השני ל-PDO הוא שם המשתמש של המסד והשלישי הסיסמה.
בשורה:
$st = $conn->prepare("SELECT * FROM Users WHERE username=? and password=?");
מופיעה המתודה prepare שיוצרת שאילתא חדשה ומקמפלת אותה לטיפוס נתונים פנימי שהוא יעיל יותר. סימני השאלה הם החלק החשוב והם משמשים כמחזיקי מקום לערכים שלאחר מכן ימולאו באמצעות המתודה bindParam.
השורה לאחר מכן:
$st->setFetchMode(PDO::FETCH_ASSOC);
אומרת ל-PDO שאנחנו מעוניינים לקבל את הערכים בתור מילון של מפתחות וערכים, זה החלופה ל-mysql_fetch_assoc.
בשורות הבאות אנחנו משתמשים ב-bindParam בשביל להחליף את סימני השאלה עם ערכים, כאשר הארגיומנט הראשון הוא מספר סימן השאלה (ראשון, שני, שלישי...) והשני הוא הערך שאותו אנחנו מעוניינים להחליף.
עד עכשיו הכנו את השאילתא, אך רק כאשר נשתמש במתודה execute השאילתא תרוץ על השרת.
השיטה הזה מונעת SQL Injection לחלוטין והיא המומלצת ביותר לשימוש. ישנם עוד מתודות רבות ואופציות ל-PDO ואני ממליץ לקרוא את הדוקומנטציה הרשמית שמופיעה ב-http://php.net/manual/en/book.pdo.php
תגובות לכתבה:
מדריך מצוין ביותר. כתוב בצורה ברורה ומובנת, מעוצב בפסקאות עם כותרות כפי שצריך והכי חשוב - באמת מסביר מה זה sql injection ואיך להגן מפניה.
עד עכשיו אף פעם לא חשבתי על זה ש mysql_real_escape_string יהיה מסוכן עבור פרמטרים מספריים, למרות שעם mysql אני משתמש בו וב-intval להבחרת הקלט.
כמובן שאני בעד pdo או mysqli במקרים קטנים.
תודה רבה, מדריך מעולה.
אגב, אשמח אם תפרט בתגובה/בפורום על union select/union select all.
כי אני משתמש בזה המון להזרקת SQL, אך באמת שאין לי מושג מה קורה שם מאחורי הקלעים, אשמח מאוד להסבר על שתי הפונקציות. תודה!
מאמר נחמד מאוד (:
גם אני ממליץ לעבור לעבוד עם MySQLi או PDO ולא רק במקרים קטנים אלכס .
union מאפשרת לך לאחד מספר שאילתות select ולקבל את הנתונים בתור טבלה אחת. union all עושה אותו הדבר רק שהיא לא מסירה תוצאות כפולות. תיצור שתי טבלאות ותתנסה אחרי שתקרא את הדוקומנטציה הרשמית של union פה -> http://dev.mysql.com/doc/refman/5.0/en/union.html
בגלל שברוב הזמן אנחנו לא יודעים את שמות השדות אנחנו משתמשים במספרים/null בשביל לא לקבל שגיאה ולגלות את מספר העמודות. mysql פשוט ישים את המספר שלנו בתשובה. לאחר שיש לנו את מספר העמודות אנחנו יכולים להשתמש בפונקציות מיוחדות של בסיס הנתונים ובסיסי נתונים/טבלאות מיוחדים בשביל לקבל עוד מידע על מבנה בסיס הנתונים או על קבצים מיוחדים שיש בשרת כמו /etc/passwd או במקרים מסויימים אפילו העלאת קבצים באמצעות select into outfile.
כל הדברים האלה נמצאים בדוקומנטציה הרשמית של MySQL שמכילה הרבה מידע מעניין.
ומה יקרה במצב ובו יש לי בשאילתא סימן שאלה שאני לא רוצה להחליף במשתנה?
זה לא משנה. PDO עושה הפרדה מלאה בין ערכים לבין קוד, זה בדיוק ההבדל בין parameterised statements ו-mysql_real_escape_query, שהוא לא מבריח תווים אלא מתייחס אליהם כנפרדים לחלוטין.
מתקפות שקשורות בהזרקת קוד כמו SQL Injection מתבססים על העובדה שיש שתי שפות סקריפט שהאחת רצה מהשניה ואתה מבלבל את המפרש של אחת מהן. במקרה הזה את המפרש SQL. מה ש-parameterised statements עושים זה מקמפלים את השאילתא לטיפוס נתונים פנימי ולאחר מכן שמים ערכים כך שבסיס הנתונים יודע בדיוק מה ההבדל בין תוכן לבין קוד. זה כמו לכתוב קוד SQL בתוך קובץ text, יש לך רק שפה אחת שמעורבת בזה SQL ואתה שם לב מתי אתה כותב קוד ומתי יש ערכים.
תודה רבה,
אתה בהחלט צודק. חובה להזהר מפריצות כאלו ואחרות, ולהגן על האתר :).
תודה רבה על המדריך !
עם אני לא מעוניין בOOP?
יכול להיות שיעניין אותך לקרוא את מה שכתוב בכתבה.
הגנה מפני SQL Injection
תת סעיף mysql_real_escape_string
מתי כבר אנשים ילמדו שרכיב ישן והפונקציות: "_mysql" מתו!
צריך להשתמש בPDO או ב MySQLi
ואז לא היו דאגות לSQL inj.
בכל מקרה כל הכבוד.
סתם מעניין לדעת, מה ההבדל בין PDO לבין MySqli?
הראשון עובד עם הרבה מדסי נתונים
השני מיועד ספציפית ל mysqli ומכאן מבצע דברים בהתאמה ויעילות רבה יותר.
תודה רבה,נותן המון מידע - מעולה!